> ## Documentation Index
> Fetch the complete documentation index at: https://mintlify.com/jaypopat/cf_ai_duet/llms.txt
> Use this file to discover all available pages before exploring further.

# SSH Server

> SSH server implementation using Charm's Wish middleware framework

Duet's SSH server is built on Charm's [Wish](https://github.com/charmbracelet/wish) framework, which provides a composable middleware system for building SSH applications.

## Server Structure

The server is defined in `/internal/server/server.go`:

```go theme={null}
type Server struct {
    addr        string
    hostKeyPath string
    roomManager *room.Manager
    logger      *log.Logger
}
```

### Initialization

The server is created with connection details and a room manager instance:

```go theme={null}
func New(addr, hostKeyPath, workerURL string) *Server {
    logger := log.NewWithOptions(os.Stderr, log.Options{
        Prefix: "duet",
    })

    var aiClient *ai.Client
    if workerURL != "" {
        aiClient = ai.NewClient(workerURL)
    }

    mgr := room.NewManager(workerURL, aiClient, logger)

    return &Server{
        addr:        addr,
        hostKeyPath: hostKeyPath,
        roomManager: mgr,
        logger:      logger,
    }
}
```

**Key Points:**

* Default address is `:2222`
* Host key path defaults to `.ssh/id_ed25519` (auto-generated if missing)
* AI client is optional (only created if worker URL provided)
* Room manager is shared across all connections

## Wish Server Configuration

The server is configured with Wish middleware:

```go theme={null}
func (s *Server) Start() error {
    srv, err := wish.NewServer(
        wish.WithAddress(s.addr),
        wish.WithHostKeyPath(s.hostKeyPath),
        wish.WithMiddleware(
            bubbletea.Middleware(s.teaHandler),
            logging.Middleware(),
        ),
    )
    if err != nil {
        return fmt.Errorf("failed to create server: %w", err)
    }
    // ...
}
```

### Middleware Stack

Duet uses two middleware layers:

1. **Bubble Tea Middleware**: Handles TUI creation for each SSH session
2. **Logging Middleware**: Logs connection events and errors

Middleware executes in order from top to bottom. The Bubble Tea middleware is the primary handler.

## Bubble Tea Handler

The `teaHandler` function is called for each new SSH connection:

```go theme={null}
func (s *Server) teaHandler(sess ssh.Session) (tea.Model, []tea.ProgramOption) {
    username := sess.User()
    if username == "" {
        username = "guest"
    }
    renderer := bubbletea.MakeRenderer(sess)

    pty, _, _ := sess.Pty()

    if pty.Term == "xterm-ghostty" {
        renderer.SetColorProfile(termenv.TrueColor)
    }

    s.logger.Info("final renderer",
        "profile", renderer.ColorProfile(),
        "hasDark", renderer.HasDarkBackground(),
    )
    return ui.New(renderer, s.roomManager, username), []tea.ProgramOption{
        tea.WithAltScreen(),
    }
}
```

### Session Information

* **Username**: Extracted from SSH session (defaults to "guest")
* **Renderer**: Created per-session for style rendering
* **PTY Info**: Terminal type and color profile detection
* **Alt Screen**: Uses alternate screen buffer (terminal state is preserved on exit)

### Color Profile Detection

The handler detects terminal capabilities:

```go theme={null}
if pty.Term == "xterm-ghostty" {
    renderer.SetColorProfile(termenv.TrueColor)
}
```

This ensures proper color rendering for clients with different terminal emulators.

## Lifecycle Management

### Graceful Shutdown

The server listens for interrupt signals and shuts down gracefully:

```go theme={null}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
defer stop()

go func() {
    s.logger.Info("Starting SSH server", "address", s.addr)
    if err := srv.ListenAndServe(); err != nil {
        s.logger.Error("Server error", "error", err)
    }
}()

<-ctx.Done()

s.logger.Info("Shutting down...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

return srv.Shutdown(shutdownCtx)
```

**Shutdown Process:**

1. Wait for SIGINT or SIGTERM signal
2. Log shutdown message
3. Create 10-second timeout context
4. Call `srv.Shutdown()` to close active connections
5. Return any shutdown errors

### Connection Lifecycle

```
SSH Client Connect
       ↓
Wish accepts connection
       ↓
Middleware stack processes session
       ↓
teaHandler creates Bubble Tea model
       ↓
tea.NewProgram(model).Run()
       ↓
Model.Init() → ScreenLaunch
       ↓
User interacts (create/join room)
       ↓
User disconnects or quits
       ↓
Model.cleanup() called
       ↓
room.LeaveRoom() removes client
       ↓
SSH session closes
```

## Authentication

Duet uses **public key authentication** via the host key:

```bash theme={null}
# Start server (generates host key if missing)
./duet -addr :2222 -hostkey .ssh/id_ed25519

# Connect as any user (username becomes display name)
ssh user@localhost -p 2222
```

### Host Key Generation

Wish automatically generates an Ed25519 host key if the file doesn't exist:

```go theme={null}
wish.WithHostKeyPath(s.hostKeyPath)
```

The key is stored at the specified path and reused for subsequent starts.

## Per-Session Isolation

Each SSH session gets:

* **Unique Bubble Tea model instance** (`ui.New(...)`)
* **Unique client ID** (`uuid.New().String()`)
* **Separate event channel** for room notifications
* **Independent terminal subscription** when joining a room

**Shared across sessions:**

* **Room Manager** (singleton)
* **Terminal instances** (one per room, shared by all clients in that room)
* **AI Client** (if configured)

## Configuration Options

Command-line flags in `main.go`:

```go theme={null}
addr := flag.String("addr", ":2222", "SSH server address")
hostKeyPath := flag.String("hostkey", ".ssh/id_ed25519", "Path to SSH host key")
workerURL := flag.String("worker", "", "Duet CF Worker base URL")
```

### Example Usage

```bash theme={null}
# Default configuration
./duet

# Custom port and host key
./duet -addr :3000 -hostkey /etc/duet/host_key

# With AI worker integration
./duet -worker https://duet-worker.example.workers.dev
```

## Error Handling

The server handles errors at multiple levels:

```go theme={null}
// Server creation errors
if err := srv.ListenAndServe(); err != nil {
    s.logger.Error("Server error", "error", err)
}

// Shutdown errors
if err := srv.Shutdown(shutdownCtx); err != nil {
    return err
}
```

Client-level errors are handled in the Bubble Tea model's `Update` method via `ErrorMsg` messages.

## Security Considerations

* **Host Key**: Securely store the host key file with appropriate permissions (0600)
* **No Password Auth**: Only public key authentication is supported
* **Workspace Isolation**: Each room uses a separate workspace directory
* **PTY Restrictions**: Terminal processes run in isolated directories

For production deployments, consider:

* Rate limiting connections
* Authentication via SSH keys
* Network-level access controls
* Monitoring and logging
